﻿using System;
using System.Reflection;

namespace TypeBuilderFx.Core.Emitters
{
    /// <summary>Emits an adapter for itemizers.</summary>
    /// <remarks>
    /// <para>
    /// An indexer is any object having an itemizer (special property <c>[]</c>) taking a string and
    /// returning an <see cref="object"/>.
    /// </para>
    /// <para>
    /// This emitter emits a type exposing a strongly type interface consisting only of properties.
    /// The properties simply forwards to the indexer using its itemizer.
    /// This gives a strongly type interface to an otherwise loosely type.
    /// </para>
    /// </remarks>
    public class ItemizerAdapterEmitter : TypeEmitterBase<ItemizerAdapterOptions>
    {
        private readonly PropertyInfo _itemProperty;

        /// <summary>Protected constructor.</summary>
        /// <param name="options"></param>
        protected ItemizerAdapterEmitter(ItemizerAdapterOptions options)
            : base(options)
        {
            _itemProperty = GetItemizerProperty(options.AdapteeType);
        }

        #region Factory Methods
        /// <summary>Creates a factory of instance of itemizer adapter.</summary>
        /// <typeparam name="INTER">Interface type of the adapter.</typeparam>
        /// <typeparam name="ADAPTEE">Type of the itemizer owner.</typeparam>
        /// <returns>A factory of instance of itemizer adapter.</returns>
        public static IFactory<INTER, ADAPTEE> GetFactory<INTER, ADAPTEE>()
            where ADAPTEE : class
            where INTER : class
        {
            return GetFactory<INTER, ADAPTEE, object>();
        }

        /// <summary>Creates a factory of instance of itemizer adapter with a given base class.</summary>
        /// <typeparam name="INTER">Interface type of the adapter.</typeparam>
        /// <typeparam name="ADAPTEE">Type of the itemizer owner.</typeparam>
        /// <typeparam name="BASE">Type of the base class.</typeparam>
        /// <returns>A factory of instance of itemizer adapter.</returns>
        public static IFactory<INTER, ADAPTEE> GetFactory<INTER, ADAPTEE, BASE>()
            where ADAPTEE : class
            where INTER : class
            where BASE : class
        {
            ItemizerAdapterOptions options = new ItemizerAdapterOptions
            {
                ExposedInterfaceType = typeof(INTER),
                AdapteeType = typeof(ADAPTEE),
                BaseType = typeof(BASE)
            };
            object untypedFactory = GetFactory(options);
            IFactory<INTER, ADAPTEE> factory = untypedFactory as IFactory<INTER, ADAPTEE>;

            if (factory == null)
            {
                throw new ApplicationException("Unable to cast the factory to the proper factory type");
            }

            return factory;
        }

        /// <summary>
        /// Creates a factory of instance of itemizer adapter with a given base class taking a parameter in construction.
        /// </summary>
        /// <typeparam name="INTER">Interface type of the adapter.</typeparam>
        /// <typeparam name="ADAPTEE">Type of the itemizer owner.</typeparam>
        /// <typeparam name="BASE">Type of the base class.</typeparam>
        /// <typeparam name="BASE_CTOR_PARAM">Type of the base class constructor parameter.</typeparam>
        /// <returns>A factory of instance of itemizer adapter.</returns>
        public static IFactory<INTER, ADAPTEE, BASE_CTOR_PARAM> GetFactory<INTER, ADAPTEE, BASE, BASE_CTOR_PARAM>()
            where ADAPTEE : class
            where INTER : class
            where BASE : class
        {
            ItemizerAdapterOptions options = new ItemizerAdapterOptions
            {
                ExposedInterfaceType = typeof(INTER),
                AdapteeType = typeof(ADAPTEE),
                BaseType = typeof(BASE),
                BaseConstructorParameterTypeList = new Type[] { typeof(BASE_CTOR_PARAM) }
            };
            object untypedFactory = GetFactory(options);
            IFactory<INTER, ADAPTEE, BASE_CTOR_PARAM> factory = untypedFactory as IFactory<INTER, ADAPTEE, BASE_CTOR_PARAM>;

            if (factory == null)
            {
                throw new ApplicationException("Unable to cast the factory to the proper factory type");
            }

            return factory;
        }

        /// <summary>Creates a factory of instance of itemizer adapter given options.</summary>
        /// <param name="options"></param>
        /// <returns></returns>
        public static object GetFactory(ItemizerAdapterOptions options)
        {
            if (options == null)
            {
                throw new ArgumentNullException("options");
            }

            ItemizerAdapterEmitter emitter = new ItemizerAdapterEmitter(options);
            object factory = options.Cache.GetFactory(emitter);

            return factory;
        }
        #endregion

        /// <summary>Emits a factory corresponding to the state of this emitter.</summary>
        /// <param name="componentTypeName"></param>
        /// <param name="factoryTypeName"></param>
        /// <param name="moduleBuilder"></param>
        /// <returns></returns>
        public override object EmitFactory(
            string componentTypeName,
            string factoryTypeName,
            ModuleBuilderExtender moduleBuilder)
        {
            Type adapterInterfaceType = GetAdapterInterfaceType(Options.AdapteeType);
            TypeBuilderExtender typeBuilder = moduleBuilder.CreateType(
                componentTypeName,
                Options.BaseType,
                adapterInterfaceType,
                Options.ExposedInterfaceType);
            Type[] baseClassInterfaces = Options.IsUsingBaseClassInterfaces
                ? TypeHelper.GetAllInterfaces(Options.BaseType, true)
                : new Type[0];

            ImplementAdapterInterface(typeBuilder, Options.AdapteeType);
            DefineAdapterConstructor(
                typeBuilder, Options.AdapteeType, Options.BaseType, Options.BaseConstructorParameterTypeList);

            ImplementInterface(
                typeBuilder,
                Options.ExposedInterfaceType,
                EmitGetMethod,
                EmitSetMethod,
                null,
                null,
                null,
                TypeHelper.Concat(baseClassInterfaces, adapterInterfaceType));

            Type componentType = typeBuilder.TypeBuilder.CreateType();
            object factory =
                CreateFactoryInstance(factoryTypeName, componentType, Options.ExposedInterfaceType, moduleBuilder);

            return factory;
        }

        private void EmitGetMethod(ILGeneratorExtender ilGenerator, MethodInfo method, PropertyInfo property)
        {
            if (!_itemProperty.CanRead)
            {
                throw new ApplicationException(
                    "The itemizer doesn't support read operation, but it is required to implement " + method.Name);
            }

            ilGenerator.StackField(ADAPTEE_FIELD_NAME);
            ilGenerator.StackValue(property.Name);
            ilGenerator.CallMethod(_itemProperty.GetGetMethod());
            ilGenerator.Cast(property.PropertyType);
            ilGenerator.ReturnStack();
        }

        private void EmitSetMethod(ILGeneratorExtender ilGenerator, MethodInfo method, PropertyInfo property)
        {
            if (!_itemProperty.CanWrite)
            {
                throw new ApplicationException(
                    "The itemizer doesn't support read operation, but it is required to implement " + method.Name);
            }

            ilGenerator.StackField(ADAPTEE_FIELD_NAME);
            ilGenerator.StackValue(property.Name);
            ilGenerator.StackMethodParameter(1);
            ilGenerator.BoxIfValueType(property.PropertyType);
            ilGenerator.CallMethod(_itemProperty.GetSetMethod());
            ilGenerator.ReturnStack();
        }

        private static PropertyInfo GetItemizerProperty(Type adapteeType)
        {
            PropertyInfo property =
                adapteeType.GetProperty(ITEM_PROPERTY_NAME, typeof(object), new Type[] { typeof(string) });

            if (property == null)
            {
                throw new ArgumentException(
                    "This type doesn't expose an itemizer taking a string and returning an object.",
                    "adapteeType");
            }

            return property;
        }
    }
}